<template>
  <a-card title="S3 分块上传" style="width: 100%">
    <a-upload-dragger
      :multiple="false"
      :before-upload="beforeUpload"
      :show-upload-list="false"
      :disabled="uploading"
    >
      <p class="ant-upload-drag-icon">
        <InboxOutlined />
      </p>
      <p class="ant-upload-text">点击或拖拽文件到此处上传</p>
      <p class="ant-upload-hint">支持断点续传</p>
    </a-upload-dragger>

    <div v-if="file" class="upload-progress">
      <a-progress
        :percent="totalProgress"
        :status="uploadStatus"
        :stroke-color="['#108ee9', '#87d068']"
      />
      <div class="upload-info">
        <span>{{ file.name }} ({{ formatFileSize(file.size) }})</span>
        <a-button
          v-if="uploading"
          type="danger"
          size="small"
          @click="cancelUpload"
        >
          取消上传
        </a-button>
      </div>
    </div>
  </a-card>
</template>

<script setup lang="ts">
import { InboxOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import axios from 'axios';
import { ref } from 'vue';

interface UploadPart {
  PartNumber: number;
  ETag?: string;
  progress: number;
}

interface UploadState {
  fileKey: string;
  uploadId?: string;
  fileName: string;
  fileSize: number;
  parts: UploadPart[];
}

const CHUNK_SIZE = 20 * 1024 * 1024; // 5MB
let LOCAL_STORAGE_KEY = 's3-upload-state';

const file = ref<File | null>(null);
const uploading = ref(false);
const totalProgress = ref(0);
const uploadStatus = ref<'active' | 'exception' | 'success'>('active');
const cancelController = ref<AbortController | null>(null);

// 生成文件唯一标识
const generateFileKey = (file: File) => {
  LOCAL_STORAGE_KEY = file.name
  return `${file.name}-${file.size}-${file.lastModified}`;
};

// 加载上传状态
const loadUploadState = (fileKey: string): UploadState | null => {
  const state = localStorage.getItem(LOCAL_STORAGE_KEY);
  return state && JSON.parse(state)?.fileKey === fileKey ? JSON.parse(state) : null;
};

// 保存上传状态
const saveUploadState = (state: UploadState) => {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
};

// 初始化上传
const initiateUpload = async (file: File) => {
  const response = await axios.post('/api/initiate-upload', {
    fileName: file.name,
    fileType: file.type
  });
  return response.data.uploadId;
};

// 上传分块
const uploadPart = async (
  file: File,
  uploadId: string,
  partNumber: number,
  chunk: Blob,
  onProgress: (progress: number) => void
) => {
  const { data: { presignedUrl } } = await axios.post('/api/get-presigned-url', {
    fileName: file.name,
    uploadId,
    partNumber
  });

  return axios.put(presignedUrl, chunk, {
    onUploadProgress: (progressEvent) => {
      if (progressEvent.total) {
        onProgress(Math.round((progressEvent.loaded / progressEvent.total) * 100));
      }
    },
    signal: cancelController.value?.signal
  });
};

// 处理上传
const handleUpload = async (file: File) => {
  const fileKey = generateFileKey(file);
  
  let state = loadUploadState(fileKey) || {
    fileKey,
    fileName: file.name,
    fileSize: file.size,
    parts: []
  };
  console.log(state)

  try {
    if (!state.uploadId) {
      state.uploadId = await initiateUpload(file);
      saveUploadState(state);
    }

    const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
    const uploadedParts = await getUploadedParts(file.name, state.uploadId);

    for (let i = 0; i < chunkCount; i++) {
      const partNumber = i + 1;
      const existingPart = uploadedParts.find(p => p.PartNumber === partNumber);

      if (!existingPart) {
        const start = i * CHUNK_SIZE;
        const end = Math.min(start + CHUNK_SIZE, file.size);
        const chunk = file.slice(start, end);

        const response = await uploadPart(file, state.uploadId!, partNumber, chunk, (progress) => {
          updatePartProgress(partNumber, progress, state);
        });

        state.parts.push({
          PartNumber: partNumber,
          ETag: response.headers.etag,
          progress: 100
        });
      } else {
        updatePartProgress(partNumber, 100, state);
      }

      saveUploadState(state);
      updateTotalProgress(state);
    }

    await completeUpload(file.name, state.uploadId, state.parts);
    uploadStatus.value = 'success';
    message.success('上传成功');
    localStorage.removeItem(LOCAL_STORAGE_KEY);
  } catch (error) {
    if (axios.isCancel(error)) {
      message.warning('上传已取消');
    } else {
      uploadStatus.value = 'exception';
      message.error('上传失败');
    }
    throw error;
  }
};

// 辅助函数
const getUploadedParts = async (fileName: string, uploadId: string) => {
  const { data } = await axios.post('/api/list-parts', { fileName, uploadId });
  return data.parts;
};

const completeUpload = async (fileName: string, uploadId: string, parts: UploadPart[]) => {
  await axios.post('/api/complete-upload', {
    fileName,
    uploadId,
    parts: parts.sort((a, b) => a.PartNumber - b.PartNumber)
  });
};

const updatePartProgress = (partNumber: number, progress: number, state: any) => {
  const part = state.parts.find(p => p.PartNumber === partNumber);
  if (part) part.progress = progress;
};

const updateTotalProgress = (state) => {
  const total = state.parts.reduce((sum, part) => sum + part.progress, 0);
  totalProgress.value = Math.round(total / state.parts.length);
};

// 上传处理
const beforeUpload = (file: File) => {
  uploadStatus.value = 'active';
  uploading.value = true;
  handleUpload(file).finally(() => {
    uploading.value = false;
  });
  return false;
};

// 取消上传
const cancelUpload = () => {
  if (cancelController.value) {
    cancelController.value.abort();
    cancelController.value = null;
  }
  uploading.value = false;
  uploadStatus.value = 'exception';
};

// 格式化文件大小
const formatFileSize = (bytes: number) => {
  const sizes = ['B', 'KB', 'MB', 'GB'];
  if (bytes === 0) return '0 B';
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
  return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
};
</script>

<style scoped>
.upload-progress {
  margin-top: 16px;
}
.upload-info {
  margin-top: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>